Overview

I'm learning JavaScript (again, and for real this time, I hope). After reading JavaScript: The Good Parts and several online pages, I set out to try some basic TDD in JavaScript. For these instructions (and the soon to be created instructional video), I'm using the following tool set:
  • Chrome (configured for auto updating)
  • Terminal (OS X Lion)
  • Java (to execute use the test runner)
  • Eclipse (to edit my files)
  • ViPlugin in Eclipse

This is my first stab at this and I hope to both get a better workflow and better at JavaScript.

Getting Setup

Setup JsTestDriver. Instructions summarized here for convenience.
% mkdir -p RpnCalcDemo/src
% mkdir -p RpnCalcDemo/src-test
% cd RpnCalcDemo 
  • For convenience, I copied the downloaded JsTestDriver jar to this directory. The name of the version I downloaded is: JsTestDriver-1.3.3d.jar
  • Create basic configuration file:

jsTestDriver.conf

server: http://localhost:9876
 
load:
  - src/*.js
  - src-test/*.js
  • In a separate terminal instance, start the test runner:
% java -jar JsTestDriver-1.3.3d.jar --port 9876
  • Open up browser/create a tab and then browse to localhost:9876.
  • Click on the Capture This Browser link. You'll see a window with the following information:
                 JsTestDriver
Last:1326495229968 | Next:1997 | Server:Waiting...

First Test: Initial X Value is 0

  • Create a first test in the src-test directory:

src-test/rpn_calculator_test.js

rpn_calculator_test = TestCase("rpn_calculator");
 
rpn_calculator_test.prototype.testShouldInitiallyBe0 = function() {
    var calc = rpn_calculator();
    assertEquals(0, calc.x());
};
  • Run your tests (they fai):
% java -jar JsTestDriver-1.3.3d.jar --tests all
java.lang.IllegalArgumentException: The patterns/paths /Users/Thoughtworks/src
/workspaces/ RpnCalcDemo/src (/Users/Thoughtworks/src/workspaces/RpnCalcDemo/
src)  used in the configuration file didn't match any file, the files patterns/
paths need to be relative /Users/Thoughtworks/src/workspaces/RpnCalcDemo
    at com.google.jstestdriver.PathResolver.expandGlob(PathResolver.java:170)
    at com.google.jstestdriver.PathResolver.resolve(PathResolver.java:109)
    at com.google.jstestdriver.config.ParsedConfiguration.resolvePaths(
ParsedConfiguration.java:98)
    at com.google.jstestdriver.config.Initializer.initialize(Initializer.java:84)
    at com.google.jstestdriver.JsTestDriver.runConfigurationWithFlags(
JsTestDriver.java:259)
    at com.google.jstestdriver.JsTestDriver.runConfiguration(
JsTestDriver.java:211)
    at com.google.jstestdriver.JsTestDriver.main(JsTestDriver.java:144)
Unexpected Runner Condition: The patterns/paths /Users/Thoughtworks/src/
workspaces/RpnCalcDemo/src (/Users/Thoughtworks/src/workspaces/RpnCalcDemo/src)
 used in the configuration file didn't match any file, the files patterns/
paths need to be relative /Users/Thoughtworks/src/workspaces/RpnCalcDemo
 Use --runnerMode DEBUG for more information.
  • The system complains that the pattern "- src/*.js" does not match any files. So fix that:

src/rpn_calculator.js

var rpn_calculator = function() {
    var that = {};
    that.x = function() {
        return 0;
    };
    return that;
};
  • Make a directory called logs and send (JUnit-formated XML-based) test output results there:
% mkdir logs
% java -jar JsTestDriver-1.3.3d.jar --tests all --testOutput logs
.
Total 1 tests (Passed: 1; Fails: 0; Errors: 0) (1.00 ms)
  Chrome 16.0.912.75 Mac OS: Run 1 tests (Passed: 1; Fails: 0; Errors 0) (1.00 ms)
% ls logs
TEST-Chrome_16091275_Mac_OS.rpn_calculator.xml
  • The resulting log file on my machine:
<?xml version="1.0" encoding="UTF-8"?>
<testsuite name="Chrome_16091275_Mac_OS.rpn_calculator" errors="0" failures="0" tests="1" time="0.0010">
<testcase classname="Chrome_16091275_Mac_OS.rpn_calculator" name="testShouldInitiallyBe0" time="0.0010"/>
</testsuite>

Next Test: Last Value Entered is X

  • Add a second test:

append to src-test/rpn_calculator_test.js

rpn_calculator_test.prototype.testXShouldBeLastValueEntered = function() {
  var calc = rpn_calculator();
  calc.enter(42);
  assertEquals(42, calc.x());
};
  • See the tests fail:
% java -jar JsTestDriver-1.3.3d.jar --tests all --testOutput logs
.E
Total 2 tests (Passed: 1; Fails: 0; Errors: 1) (0.00 ms)
  Chrome 16.0.912.75 Mac OS: Run 2 tests (Passed: 1; Fails: 0; Errors 1) (0.00 ms)
    rpn_calculator.testXShouldBeLastValueEntered error (0.00 ms): TypeError: 
Object #<Object> has no method 'enter'
      TypeError: Object #<Object> has no method 'enter'
          at [object Object].testXShouldBeLastValueEntered (http://localhost:9876/test
/src-test/rpn_calculator_test.js:10:7)
 
Tests failed: Tests failed. See log for details.
  • Add the missing method (and update the implementation a touch):

src/rpn_calculator.js

var rpn_calculator = function() {
    var that = {};
    that.value = 0;
    that.x = function() {
        return that.value;
    };
    that.enter = function(value) {
        that.value = value;
    };
    return that;
};
  • Run the tests, see the pass:
% java -jar JsTestDriver-1.3.3d.jar --tests all --testOutput logs
..
Total 2 tests (Passed: 2; Fails: 0; Errors: 0) (0.00 ms)
  Chrome 16.0.912.75 Mac OS: Run 2 tests (Passed: 2; Fails: 0; Errors 0) (0.00 ms)
  • Refactor the rpn_calculator function, hide value:
var rpn_calculator = function() {
    var that = {};
    var value = 0;
    that.value = 0;
    that.x = function() {
        return value;
    };
    that.enter = function(newValue) {
        value = newValue;
    };
    return that;
};
  • Check, tests still pass?
% java -jar JsTestDriver-1.3.3d.jar --tests all --testOutput logs
..
Total 2 tests (Passed: 2; Fails: 0; Errors: 0) (1.00 ms)
  Chrome 16.0.912.75 Mac OS: Run 2 tests (Passed: 2; Fails: 0; Errors 0) (1.00 ms)
  • Refactor the tests, remove duplication (this is multiple refactorings on my end):
rct = {};
rpn_calculator_test = TestCase("rpn_calculator", {
    setUp : function() {
        rct.calc = rpn_calculator();
    },
    tearDown : function() {
        rct.calc = undefined;
    }
});
 
rpn_calculator_test.prototype.testShouldInitiallyBe0 = function() {
    assertEquals(0, rct.calc.x());
};
 
rpn_calculator_test.prototype.testXShouldBeLastValueEntered = function() {
    rct.calc.enter(42);
    assertEquals(42, rct.calc.x());
};
  • Verify the tests still pass:
% java -jar JsTestDriver-1.3.3d.jar --tests all --testOutput logs
..
Total 2 tests (Passed: 2; Fails: 0; Errors: 0) (1.00 ms)
  Chrome 16.0.912.75 Mac OS: Run 2 tests (Passed: 2; Fails: 0; Errors 0) (1.00 ms)

Can Handle Multiple Numbers Entered

  • Now create a new test:

Appended to src-test/rpn_calculator_test.js

rpn_calculator_test.prototype.testStoresMultipleValues = function() {
  rct.calc.enter(42);
  rct.calc.enter(9);
  rct.calc.drop();
  assertEquals(42, rct.calc.x());
};
  • This test will fail because there is no drop() method nor is this functionality supported.
% java -jar JsTestDriver-1.3.3d.jar --tests all --testOutput logs
..E
Total 3 tests (Passed: 2; Fails: 0; Errors: 1) (0.00 ms)
  Chrome 16.0.912.75 Mac OS: Run 3 tests (Passed: 2; Fails: 0; Errors 1) (0.00 ms)
    rpn_calculator.testStoresMultipleValues error (0.00 ms): TypeError: Object #<Object> 
has no method 'drop'
      TypeError: Object #<Object> has no method 'drop'
          at Object.testStoresMultipleValues (http://localhost:9876/test/src-test/
rpn_calculator_test.js:23:11)
 
Tests failed: Tests failed. See log for details.
  • After a little effort, we have this:

src/rpn_calculator.js

var rpn_calculator = function() {
    var that = {};
    var values = [0];
    that.value = values;
    that.x = function() {
        return values[values.length-1];;
    };
    that.enter = function(newValue) {
        values.push(newValue);
    };
    that.drop = function() {
        values.pop()
    };
    return that;
};
  • And the test are back to passing:
 
% java -jar JsTestDriver-1.3.3d.jar --tests all --testOutput logs
...
Total 3 tests (Passed: 3; Fails: 0; Errors: 0) (1.00 ms)
  Chrome 16.0.912.75 Mac OS: Run 3 tests (Passed: 3; Fails: 0; Errors 0) (1.00 ms)

One More Check

  • Notice the feature envy in the x function? I uses values the variable, the length of values and also has direct knowledge that the size is 0-based. This is not a huge deal, but there are defects in the system as written. Here's a test to demonstrate a problem:
rpn_calculator_test.prototype.testCalculatorAlwaysHasValues = function() {
  rct.calc.drop();
  rct.calc.drop();
  assertEquals(0, rct.calc.x());
};
  • Notice the failing test:
java -jar JsTestDriver-1.3.3d.jar --tests all --testOutput logs
...F
Total 4 tests (Passed: 3; Fails: 1; Errors: 0) (1.00 ms)
  Chrome 16.0.912.75 Mac OS: Run 4 tests (Passed: 3; Fails: 1; Errors 0) (1.00 ms)
    rpn_calculator.testCalculatorAlwaysHasValues failed (0.00 ms): AssertError: expected 0 
but was [undefined]
      AssertError: expected 0 but was [undefined]
          at Object.testCalculatorAlwaysHasValues (http://localhost:9876/test/src-test/
rpn_calculator_test.js:30:2)
  • On both of my 2 HP calculators, I can drop all day long and nothing much happens (somewhat simplified, but reasonable for this demonstation. Conceptually, the so-called (by the documentation) "operand stack" is never empty. Here's a way to implement that:

Update x method

  that.x = function() {
    if(values.length > 0)
      return values[values.length-1];;
    return 0;
  };
  • This gets the job done but now this method is exhibiting feature envy even stronger:
    • It checks the length twice
    • It knows that the array is 0-based
    • It uses "value." twice and values 1, so values three times.
  • A typical fix for feature envy is to push the responsibility into the object that has the data. To do that, we'll introduce a new object: rpn_stack.

rpn_stack

  • We'll begin with a few TDD cycles:

rpn_stack_test.js

rpn_stack_test = TestCase("rpn_stack");
 
rpn_stack_test.prototype.testPopOnNewStackReturns0 = function() {
    assertEquals(0, rpn_stack().pop());
};
  • This fails (there's no rpn_stack() function:
% java -jar JsTestDriver-1.3.3d.jar --tests all --testOutput logs
....E
Total 5 tests (Passed: 4; Fails: 0; Errors: 1) (1.00 ms)
  Chrome 16.0.912.75 Mac OS: Run 5 tests (Passed: 4; Fails: 0; Errors 1) (1.00 ms)
    rpn_stack.testPopOnNewStackReturns0 error (1.00 ms): ReferenceError: rpn_stack
 is not defined
      ReferenceError: rpn_stack is not defined
          at [object Object].testPopOnNewStackReturns0 (http://localhost:9876/test/
src-test/rpn_stack_test.js:4:18)
  • Fix this by making one and giving it an implementation:
var rpn_stack = function() {
    that = {};
    that.pop = function() {
        return 0;
    };
    return that;
};
  • Now the tests pass:
% java -jar JsTestDriver-1.3.3d.jar --tests all --testOutput logs
.....
Total 5 tests (Passed: 5; Fails: 0; Errors: 0) (1.00 ms)
  Chrome 16.0.912.75 Mac OS: Run 5 tests (Passed: 5; Fails: 0; Errors 0) (1.00 ms)
  • Next, we want to make sure that the last value entered is the one that pop returns:

Append new test to rpn_stack_test.js

rpn_stack_test.prototype.testPopReturnsLastValuePushed = function() {
  var values = rpn_stack();
  values.push(42);
  assertEquals(42, values.pop());
};
  • This fails, so fix it:
var rpn_stack = function() {
  that = {};
  var values = [0];
  that.pop = function() {
    return values.pop();
  };
  that.push = function(value) {
    values.push(value);
  };
  return that;
};
  • Check that it works:
......
Total 6 tests (Passed: 6; Fails: 0; Errors: 0) (0.00 ms)
  Chrome 16.0.912.75 Mac OS: Run 6 tests (Passed: 6; Fails: 0; Errors 0) (0.00 ms)
  • Do these tests seem familiar? They are almost straight out of src-test/rpn_calculator_test.js. Along those lines, here's a check similar to the last one we wrote on rpn_calculator:
rpn_stack_test.prototype.testSeveralDropsAndPopIsStill0 = function() {
  var values = rpn_stack();
  values.drop();
  values.drop();
  values.drop();
  assertEquals(0, values.pop());
};
  • Run the tests, you'll notice you the result is undefined instead of 0. This is a quick fix:

var rpn_stack = function() {
  that = {};
  var values = [];
  that.pop = function() {
    if(values.length > 0)
      return values.pop();
    return 0;
  };
  that.push = function(value) {
    values.push(value);
  };
  return that;
};
  • Run the tests, you should be back to passing.
  • A quick check of rpn_calculator.js and you'll notice that while there's a use of ".length", this has been pushed into the new rpn_stack class. However, there's also a need to get the top without removing it. So two more TDD cycles:
rpn_stack_test.prototype.testTopOfNewStack0 = function() {
  assertEquals(0, rpn_stack().top());
};
  • And top version 1:
  that.top = function() {
    return 0;
  };
  • Then something similar to what we did for pop:
rpn_stack_test.prototype.testTopReturnsLastValuePushed = function() {
  var values = rpn_stack();
  values.push(19);
  assertEquals(19, values.top());
};
  • And a fix:
  that.top = function() {
    if(values.length > 0)
      return values[values.length-1];
    return 0;
  };
  • Notice that there's some duplication between top and pop. When I know JavaScript well enough, I'll remove it. Until then, let's use this new object in rpn_calculator.

Using rpn_stack in rpn_calculator

  • Make a few updates to the rpn_calculator class:
var rpn_calculator = function() {
  var that = {};
  var values = rpn_stack();
  that.value = values; 
  that.x = function() {
    return values.top();
  };
  that.enter = function(newValue) {
    values.push(newValue);
  };
  that.drop = function() {
    values.pop()
  };  
  return that;
};
  • Run the tests, everything should be passing. Here's the full version of rpn_stack.js:

src/rpn_stack.js

var rpn_stack = function() {
  that = {}; 
  var values = [];
  that.pop = function() {
    if(values.length > 0)
      return values.pop();
    return 0;
  }
  that.push = function(value) {
    values.push(value);
  };
  that.top = function() {
    if(values.length > 0)
      return values[values.length-1];
    return 0;
  };
  return that;
};